從昨天的最後我們有說分散於各地的驗證規則不好管控,今天會透過以下的主題,整合驗證規則和驗證失敗的處理,同時再工商一個 vaidation 的套件,讓驗證規則可與前端開發共用。
我們可以從 Illuminate\Http\Request
的 api 文件中看到,Request 本身並沒有帶入驗證規則,所以我們才需要在 controller 中自行驗證。好在從 Illuminate\Foundation\Http\FormRequest
(後續簡稱 FormRequest) 我們可以看到它具有 validateResolved()
功能,這個 function 是當 FormRequest 產生實例後會去執行、驗證本身是否合法:
trait ValidatesWhenResolvedTrait
{
/**
* Validate the class instance.
*
* @return void
*/
public function validateResolved()
{
$this->prepareForValidation();
if (! $this->passesAuthorization()) {
$this->failedAuthorization();
}
$instance = $this->getValidatorInstance();
if ($instance->fails()) {
$this->failedValidation($instance);
}
}
下面是將驗證的規則跟客製的錯誤訊息寫至 FormRequest 的作法:
App\Http\Requests
的 FormRequest。php artisan make:request <Request 名稱>
class EditPostFormRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
// 預設為 false,如果不需要授權驗證,要改為 true
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}
// ...
public function rules()
{
return [
"userId" => "required|integer|exists:users,id",
"postId" => "required|integer|exists:posts,id",
"title" => "nullable|string",
"content" => "nullable|string"
];
}
messages()
方法: // ...
public function messages()
{
return [
"userId.required" => "使用者 ID 為避填資料",
"userId.exists" => '使用者 ID 必須存在於資料庫中',
"postId.integer" => "文章 ID 必須為數值",
"postId.exists" => "文章 ID 不存在"
];
}
validateResolved()
方法中可以看到,當驗證沒有通過,會執行 failedValidation()
方法,同時再往下追,可以在 FormRequest 中可以看到,除了丟出 ValidationException
之外,另外還會進行跳轉。 protected function failedValidation(Validator $validator)
{
throw (new ValidationException($validator))
->errorBag($this->errorBag)
->redirectTo($this->getRedirectUrl());
}
所以假若我們希望在驗證失敗的時候做一些處理,可以 override failedValidation()。兩個常用情境介紹:
getValidatorInstance()
改為 public,讓 controller 可以取到 validator 實例並進一步得到錯誤訊息。class EditPostFormRequest extends FormRequest
{
// ...
protected function failedValidation(Validator $validator)
{
}
// override getValidatorInstance,將 protected 改為 publick
public function getValidatorInstance()
{
return parent::getValidatorInstance();
}
}
class PostController extends Controller
{
public function edit(EditPostFormRequest $request)
{
$validator = $request->getValidatorInstance();
if ($validator->fails()) {
$errorMessage = $validator->getMessageBag()->getMessages();
// ...
}
}
}
class EditPostFormRequest extends FormRequest
{
// ...
protected function failedValidation(Validator $validator)
{
// 取得錯誤資訊
$responseData = $validator->errors();
// 產生 JSON 格式的 response,(422 是 Laravel 預設的錯誤 http status,可自行更換)
$response = response()->json($responseData, 422);
// 丟出 exception
throw new HttpResponseException($response);
}
function edit(PostService $postService, EditPostFormRequest $request)
{
$updateData = $request->only(['title', 'content']);
$userId = $request['userId'];
$postId = $request['postId'];
try {
$updatedPost = $postService->updatePost($userId, $postId, $updateData);
$updatedPost = $postService->modelToAPIResource(updatedPost);
return response()->json([
'success' => true,
'message' => null,
'data' => $updatedPost
]);
} catch (\Exception $exception) {
$exMessage = $exception.getMessage();
$exCode = $exception.getCode();
return response()->json([
'success' => false,
'message' => "catch exception:{$exMessage}",
'code' => $exCode,
], 500);
}
}
進一步的,如果希望這些驗證規則也能提供前端開發使用,不妨參考小弟之前寫的小工具 semantic-lab/lara-validator
(packagist 或 GitLab)。
config\validators
: 接下來要存放各種驗證規則的地方app\Http\Responses
: 簡易的 response 類別composer require semantic-lab/lara-validator --dev
config\validators
撰寫若干 json 設定檔,主要設定欄位如下:subNamespace
validators
failResponse
Config\validators
底下所有 json 設定檔,產出 FormRequest。php artisan validator:make
subNamespace
代表之後產生的 FormRequest 會在 app\Http\Request
下的哪一個資料夾。若沒有設定,預設一樣會在 app\Http\Request
底下,例如: "subNamespace": "Post"
,則這份檔案所有的 FormRequest 都會建立在 app\Http\Request\Post
底下。
這裡主要設定各個 FromRequest 的驗證規則和客製訊息。
validators
底下的每一個 key 都會是之後產生的 FormRequest 類別名稱body
裡面。{
"validators": {
"EditPostRequest": {
"body": {
"userId": "required|integer|exists:users,id",
"postId": "required|integer|exists:posts,id",
"title": "nullable|string",
"content": "nullable|string"
}
},
"CreatePostRequest": {
// ...
},
// ...
}
}
若要客製訊息,可以改在保留字 rules
底下設定,如果驗證方法的訊息設為 null
,產生之後會使用 Laravel 預設錯誤訊息。
{
"validators": {
"EditPostRequest": {
"body": {
"userId": {
"rules": {
"required": "使用者 ID 為必填資料",
"integer": null,
"exists:users,id": "使用者 ID 必須存在於資料庫中"
}
},
"postId": {
"rules": {
"required": null,
"integer": "文章 ID 必須為數值",
"exists:posts,id": "文章 ID 不存在"
}
},
}
},
}
}
巢狀資料的設定就是不斷以 sub-object 往下定義即可,只是需要留意要驗證的資料欄位名稱不可為 rules
保留字。
{
"validators": {
"EditPostRequest": {
"body": {
"user": {
"id": "required|integer|exists:users,id"
},
"post": {
"id": "required|integer|exists:posts,id",
"title": "nullable|string",
"content": "nullable|string"
}
}
}
}
}
陣列資料的設定,稍微特殊一些,我們會用相同的驗證規則,驗證每個陣列裡的每一個元素。
{
"validators": {
"EditMultiPostsRequest": {
"body": {
"user": {
"id": "required|integer|exists:users,id"
},
"posts": [{
"id": "required|integer|exists:posts,id",
"title": "nullable|string",
"content": "nullable|string"
}]
}
}
}
}
failResponse
是用來設定檔案內所有 validators 驗證錯誤的處理方法。
類型 | 說明 |
---|---|
default | Laravel 預設跳轉至首頁 |
ignore | 不做任何動作,依然進入到 controller function (action) 中 |
exception | 丟出 \Illuminate\Support\MessageBag 作為 response |
response | 回傳設定的 IResponse 資料 |
{
// ...
"failResponse": {
// default, ignore, exception 或 response
"type": "response",
// exception 或 response 可以指定 response 的 HTTP staus (預設為 422)
"httpStatus": 200,
// response 所需要設定的類別,且此類別必須實作 IResponse,
"class": "APIResponse"
}
}
上面的範例是採用「response (回傳設定的 IResponse 資料)」的方式,其中,class 的部分會對應到 app\Http\Response
的 APIResponse
(不是預設,需實作)。
有了 FormRequest 之後,我們可以再將驗證資料的工作,從 controller 拆出來改由 FormRequest 處理,除了 controller 更簡潔之外,進一步的我們還有機會可以重複利用 FormRequest。
Packagist 有不少驗證套件,semantic-lab/lara-validator 的部分大家可以參考看看,如果套件有問題或是有需要改進也歡迎留言或是發 issue。除了 PHP 的部分,小弟也有寫相對應的 JavaScript 套件,這部分就到 Nuxt.js 的部分再介紹了!
總之至目前為止我們已經可以完整的寫 API 了。明天我們會說明 Laravel Routing
的設定,然後就可以跑看看我們的 API 到底有沒有成功啦!